package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Data;
import com.hmdp.entity.Shop;
import com.hmdp.exception.ShopNotFoundException;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@Slf4j
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private CacheClient cacheClient;

    /**
     * 根据id查询店铺
     * @param id
     * @return
     */
    @Override
    @Transactional
    //@Cacheable(cacheNames = "shopCache",key = "#id")
    public Result findById(Long id) {
        //TODO:缓存穿透
        //Shop shop = findWithPassThrough(id);
        //return Result.ok(shop);
        Shop shop = cacheClient.findWithPassThrough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.MINUTES);


        //TODO:缓存击穿--互斥锁
//        Shop shop = findByIdWithMutex(id);
//        if (shop == null) {
//            return Result.fail("店铺不存在！");
//        }
        //TODO:缓存击穿--逻辑过期
//        Shop shop = findByIdWithLogicalExp(id);
        if (shop == null) {
            return Result.fail("店铺不存在！");
        }

        return Result.ok(shop);

    }

    /**
     * 解决缓存穿透
     * @param id
     * @return
     */
    public Shop findWithPassThrough(Long id) {

        //查询redis
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        Map shopMap = redisTemplate.opsForHash().entries(key);

        //判断是否是空值
        if (shopMap.containsKey("_empty")) {
            return null;
        }
        //命中直接返回
        if (!shopMap.isEmpty()) {
            Shop shop = BeanUtil.fillBeanWithMap(shopMap, new Shop(), false);
            return shop;
        }

        //没有命中
        //查数据库
        Shop shop = getById(id);
        if (shop == null) {
            log.info("店铺不存在");
            //参数为空map不会写入缓存！！！

            //此处设置占位字段标记
            Map<String, Object> map = new HashMap<>();
            map.put("_empty", true);
            redisTemplate.opsForHash().putAll(key,map);
            redisTemplate.expire(key,RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }

        //写入缓存
        Map<String, Object> map = BeanUtil.beanToMap(shop);
        redisTemplate.opsForHash().putAll(key, map);
        //设置超时时间
        redisTemplate.expire(key,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return shop;

    }


    /**
     * 互斥锁解决击穿
     * @param id
     * @return
     */
    public Shop findByIdWithMutex(Long id) {
        //查询redis

        String key = RedisConstants.CACHE_SHOP_KEY + id;
        Map shopMap = redisTemplate.opsForHash().entries(key);

        if (shopMap.containsKey("_empty")) {
            return null;

        }
        //有缓存 返回
        if(!shopMap.isEmpty()) {
            Shop shop = BeanUtil.fillBeanWithMap(shopMap, new Shop(), false);
            return shop;
        }
        //没有缓存  加锁查数据库
        String lockKey="lock:shop:"+id;
        try {
            while(true) {

                Boolean lock = getLock(lockKey);
                if (lock) {
                    //再查缓存
                    Map map = redisTemplate.opsForHash().entries(key);
                    //判断为空
                    if (map.containsKey("_empty")) {
                        return null;
                    }
                    //命中返回
                    if(!map.isEmpty()) {
                        return BeanUtil.fillBeanWithMap(map, new Shop(), false);
                    }

                    //没有命中 查数据库
                    Shop shop = getById(id);
                    if (shop == null) {
                        map = new HashMap<>();
                        map.put("_empty", true);
                        redisTemplate.opsForHash().putAll(key,map);
                        redisTemplate.expire(key,RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);

                        return null;
                    }
                    //写入缓存
                    shopMap = BeanUtil.beanToMap(shop);
                    redisTemplate.opsForHash().putAll(key, shopMap);
                    redisTemplate.expire(key,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
                    return shop;


                }
                if (!lock) {
                    Thread.sleep(50);
                    continue;
                }
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //释放锁
            unlock(lockKey);
        }


    }

    //线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 逻辑过期解决击穿
     * @param id
     * @return
     */
    public Shop findByIdWithLogicalExp(Long id) {
        //查询redis

        String key = RedisConstants.CACHE_SHOP_KEY + id;
        Map shopMap = redisTemplate.opsForHash().entries(key);
        //不存在，直接返回
        if (shopMap.isEmpty()) {
            return null;
        }


        //有缓存
        //判断是否过期


        RedisData data = BeanUtil.fillBeanWithMap(shopMap, new RedisData(), false);
        LocalDateTime expTime = data.getExpireTime();
        Shop cacheShop = (Shop) data.getData();
        //1.没过期 直接返回
        if(expTime.isAfter( LocalDateTime.now())){
            return (Shop) data.getData();
        }else {
        //2.过期
            String lockKey="lock:shop:"+id;
            Boolean lock = getLock(lockKey);
            if (lock) {

                //获取锁成功

                { //再查缓存
                    Map map = redisTemplate.opsForHash().entries(key);
                    data = BeanUtil.fillBeanWithMap(map, new RedisData(), false);
                    //没过期 返回
                    if (data.getExpireTime().isAfter(LocalDateTime.now())) {
                        return (Shop) data.getData();
                    }
                    //过期 查数据库更新
                    Shop shop = getById(id);
                    //空直接返回
                    if (shop == null) {
                        return null;
                    }
                }

                //开启新线程 -->查数据库更新缓存
                CACHE_REBUILD_EXECUTOR.submit(() ->{
                    //完成缓存重建任务
                    try {
                        this.saveShop2Redis(id,2L);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }finally {
                        //释放锁
                        unlock(lockKey);
                    }


                });


            }
            //返回缓存里的旧数据
            return cacheShop;

        }


    }


    /**
     * 获取锁
     * @return
     */
    public Boolean getLock(String key){
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "1", 100, TimeUnit.SECONDS);
        return result!=null? result:false;
    }

    /**
     * 释放锁
     */
    public void unlock(String key){
        redisTemplate.delete(key);

    }

    /**
     * 店铺信息写入redis
     * @param id
     */
    @Override
    public void saveShop2Redis(Long id,Long expSec){
        //提前预热 缓存热key
        //1.查询数据库
        Shop shop = getById(id);

        //2.封装 设置逻辑过期时间
        RedisData data = new RedisData();
        data.setData(shop);
        data.setExpireTime(LocalDateTime.now().plusSeconds(expSec));

        //3.写入redis
        Map<String, Object> map = BeanUtil.beanToMap(data);
        redisTemplate.opsForHash().putAll(RedisConstants.CACHE_SHOP_KEY+id,map);

    }


    @Override
    @Transactional
    public Result update(Shop shop) {
        //更新数据库
        updateById(shop);

        //删除缓存
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("店铺id不能为空！");
        }
        //删除hash类型key对应的所有值
        redisTemplate.delete(RedisConstants.CACHE_SHOP_KEY+id);

        return Result.ok();
    }
}
